package cc.android.testapp.xp.hooker;

import android.app.Application;

import org.json.JSONObject;

import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;

import cc.android.testapp.TestApp;
import cc.android.testapp.cfg.Config;
import cc.android.testapp.cfg.ConfigHelper;
import cc.android.testapp.ui.MainActivity;
import cc.android.testapp.util.CLog;
import cc.android.testapp.util.CTool;
import cc.android.testapp.xp.HookBase;
import cc.android.testapp.xp.HookModule;
import cc.commons.util.StringUtil;
import cc.commons.util.extra.CList;
import cc.commons.util.reflect.MethodUtil;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

@HookModule(name = "方法追踪")
public class HookStacktrace extends HookBase implements ConfigHelper.IConfig {

    private static ClassLoader mLoader;
    private static String mPreCheckClass = null;
    private static String mPreCheckMethod = null;
    private final static ArrayList<Unhook> mMethodHook = new ArrayList<>();

    @Override
    public void onConfigChange() {
        runHook();
    }

    @Override
    public String getCfgNode() {
        return "";
    }

    @Override
    protected void hookToApp(XC_LoadPackage.LoadPackageParam pParam, Application pApp) throws Throwable {
        mLoader = pApp.getClassLoader();
        this.onConfigChange();
    }

    public static void runHook() {
        if (mLoader == null) return;
        JSONObject tCfg = Config.getOrCreateSubCfg("track");
        String mCheckClass = tCfg.optString(Config.KEY_CHECK_CLASS, null);
        if (StringUtil.isBlank(mCheckClass)) mCheckClass = null;
        else mCheckClass = mCheckClass.replace('/', '.');
        String mCheckMethod = tCfg.optString(Config.KEY_CHECK_METHOD, null);
        if (StringUtil.isBlank(mCheckMethod)) mCheckMethod = null;

        try {
            if (Objects.equals(mCheckClass, mPreCheckClass) && Objects.equals(mCheckMethod, mPreCheckMethod))
                return;
        } finally {
            mPreCheckClass = mCheckClass;
            mPreCheckMethod = mCheckMethod;
        }

        if (!mMethodHook.isEmpty()) {
            mMethodHook.forEach(XC_MethodHook.Unhook::unhook);
            mMethodHook.clear();
        }
        if (mCheckClass == null || mCheckMethod == null) return;
        Class<?> tClazz = null;
        try {
            if (CTool.classExist(mLoader, mCheckClass)) tClazz = mLoader.loadClass(mCheckClass);
            else if (CTool.classExist(HookStacktrace.class.getClassLoader(), mCheckClass)) {
                tClazz = HookStacktrace.class.getClassLoader().loadClass(mCheckClass);
            }
        } catch (ClassNotFoundException ignore) {
        }
        if (tClazz == null) {
            CLog.log("[ST] Class no found: " + mCheckClass);
            TestApp.sendRemoteToast(MainActivity.class, "未找到该类");
            return;
        }

        final String tCM = mCheckMethod;
        try {
            CList<Member> tMethods = new CList<>();
            if (mCheckMethod.equals("<init>")) {
                tMethods.addAll(Arrays.asList(tClazz.getDeclaredConstructors()));
            } else if (MethodUtil.isMethodExist(tClazz, (pMethod) -> pMethod.getName().equals(tCM))) {
                tMethods.addAll(MethodUtil.getMethodIgnoreParam(tClazz, mCheckMethod, false));
            } else {
                do {
                    if (MethodUtil.isDeclaredMethodExist(tClazz, (pMethod) -> pMethod.getName().equals(tCM))) {
                        tMethods.addAll(MethodUtil.getMethodIgnoreParam(tClazz, mCheckMethod, true));
                        break;
                    }
                    tClazz = tClazz.getSuperclass();
                } while (tClazz != null);
            }

            if (tMethods.isEmpty()) {
                CLog.log("[ST]  Method (" + mCheckMethod + ") in Class " + mCheckClass + " no found");
                TestApp.sendRemoteToast(MainActivity.class, "类方法 \"" + mCheckMethod + "\" 不存在");
                return;
            }

            for (Member sMethod : tMethods) {
                XC_MethodHook.Unhook t = XposedBridge.hookMethod(sMethod, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {

                        try {
                            synchronized (CLog.class) {
                                CLog.FORCE_FILE_OUTPUT = true;
                                CLog.log("Stack trace for " + sMethod);
                                StringBuilder tSBuilder;

                                if (param.thisObject != null) {
                                    CLog.log("------===trace start===------");
                                    CLog.log("Fields: \n" + CTool.fieldVToStrV(param.thisObject));
                                }

                                tSBuilder = new StringBuilder("Params: ");
                                if (param.args.length != 0) {
                                    int i = 0;
                                    for (Object sObj : param.args) {
                                        if (sObj == null) {
                                            String tName = (sMethod instanceof Method) ? ((Method) sMethod).getParameterTypes()[i].getName()
                                                    : ((Constructor<?>) sMethod).getParameterTypes()[i].getName();
                                            tSBuilder.append("(" + tName + ":null)");
                                        } else {
                                            tSBuilder.append("(" + sObj.getClass().getName() + ":" + sObj + ")");
                                        }
                                        i++;
                                    }
                                } else {
                                    tSBuilder.append("(none)");
                                }
                                CLog.log(tSBuilder.toString());
                                String tResult = String.valueOf(param.getResult());
                                CLog.log("return: " + tResult);
                                CLog.log(new RuntimeException());
                                CLog.log("------===trace end===------");
                            }
                        } catch (Throwable e) {
                            CLog.log("Error on print trace method: " + e.getLocalizedMessage());
                        } finally {
                            CLog.FORCE_FILE_OUTPUT = false;
                        }
                    }
                });
                mMethodHook.add(t);
            }
            CLog.log("Start track Method (" + mCheckMethod + ") in Class " + mCheckClass);
            TestApp.sendRemoteToast(MainActivity.class, "成功设置跟踪");
        } catch (Throwable e) {
            e.printStackTrace();
            CLog.log("Error on trace hook: " + e.getLocalizedMessage());
        }

    }
}
